///----------------------------------------------------------------------------
/// File Name: Dispatcher.cs
/// 
/// Description: 
/// The Dispatcher is the workflow controller for each request received.
/// After the Dispatcher decides on the controller it and hands off the manual 
/// work of processing the request and plays no further part.
/// 
/// History:
/// 05 Mar 2004	Aaron Clauson	Created. Derived in part from Maverick.Net, 
///								http://mavnet.sourceforge.net/.
///	04 Sep 2004	Aaron Clauson	Changed the loading of commands so that strings
///								with a "command1|command2" form will be treated
///								as two seperate commands to allow for equality
///								matches.
///	24 Nov 2004 Aaron Clauson	Added the forcessl attribute to the command.
///	05 Jan 2005 Aaron Clauson	Added some additional unit tests for regex command selections.
///
/// License:
/// Public Domain.
///----------------------------------------------------------------------------

using System;
using System.Collections;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml;
using MaverickLite.Config;
using MaverickLite.Controller;
using log4net;

#if UNITTEST
using NUnit.Framework;
#endif

namespace MaverickLite
{

	
	/// <summary>
	/// The Dispatcher is the central command processor of the framework.
	/// All request are routed to this object by way of an extension mapping
	/// in the web.config.  From here requests are routed through the workflow
	/// stages of the Controller and the View.
	/// The Dispatcher is also responsible for initialising the workflow by processing
	/// the configuration file. The Dispatcher requires the schema of the file to be:
	/// 
	/// <mavericklite>
	///		<commands>
	///			<command name="command1" [forcessl="true"]>
	///				...Controller worries about contents...
	///			</command>
	///			<command name="^\w">
	///				...Controller worries about contents...
	///			</command>
	///		</commands>
	/// </mavericklite>
	/// 
	/// Command names can either be the exact name of a page request or a regular expression.
	/// Currently there is no order associated with the commands since they are stored in a hash table.
	/// If multiple expressions are used overlaps must be carefully planned for!
	/// 
	/// If forcessl is set to true the dispatcher will send a HTTP redirect to the client and no
	/// server side processing will be done.
	/// 
	/// </summary>
	public class Dispatcher : IHttpHandler
	{
		private const string COMMAND_NODES_PATH = "mavericklite/commands/command";	// XPath to command nodes in the MaverickLite configuration file.
		private const string COMMAND_NAME_ATTRIBUTE = "name";						// Name of the command node attribute specifying the command name.
		private const string SSL_REQUIRED_ATTRIBUTE = "forcessl";					// If set to true any non https requests will be redirected to https versions.

		private static ILog logger = MavAppState.logger;
		private static ILog visitorLog = MavAppState.VisitorLog;

		private static string m_physicalApplicationPath = MavAppState.PhysicalApplicationPath;		// Physcial path to the web application root.
		private static string m_configFileName = MavAppState.MaverickConfigFile;					// Determine the path to the Maverick configuration file.
		private static string m_virtualDir = MavAppState.VirtualDir;								// The virtual directory of the web application, used for regex pattern matching.
		private static string m_reloadCommand = MavAppState.ReloadCommand;							// The name of the page that when requested will cause the relaoding of all the command objects.
		private static string m_reloadCommandPattern = null;									// The regular expression pattern used to match the reload command pattern.

		private static IDictionary CommandMap;				// The list of commands (case insensitive) and their associated workflow maps in the application.
		private static XmlDocument XmlConfigDoc;			// The last loaded MaverickLite configuration document.

		/// <summary>
		/// Required by IHttpHandler.
		/// </summary>
		public bool IsReusable
		{
			get { return true; }
		}

		/// <summary>
		/// A structure containing the information necessary to process an incoming command.
		/// </summary>
		public struct PageCommand
		{
			public bool SSLRequired;				// Whether or not to send a redirect to https version of this page.
			public ControllerProduct Controller;	// Controller for the page.
		}

		/// <summary>
		/// Initialise the Dispatcher and the workflow map of commands from the configuration file.
		/// </summary>
		static Dispatcher()
		{
			try
			{		
				logger.Debug("Physical application path for the web app, " + m_physicalApplicationPath + ".");

				if(File.Exists(m_configFileName))
				{
					// Initialise the workflow map.
					Init(m_configFileName);
				}
				else
				{
					throw new ApplicationException("The application configuration file could not be found, " + m_configFileName + ".");
				}
			}
			catch(Exception excp)
			{
				logger.Error("Exception (Dispatcher constructor): " + excp.Message);
				throw excp;
			}
		}


		/// <summary>
		/// This is where the incoming request context will be handed to the Dispatcher from IIS.
		/// This method will be heavily loaded in a real application since it will be called
		/// for every page request.
		/// </summary>
		/// <param name="context">The details of the incoming request.</param>
		public void ProcessRequest(HttpContext context) 
		{			
			logger.Debug("Incoming Request: " + DateTime.Now.ToString("dd MMM yy HH:mm:ss:fff") + ", host IP = " + context.Request.UserHostAddress + ", request = " + context.Request.Path + ".");

			// Identify the current command
			string originalRequest = context.Request.RawUrl;	// Keep hold of this in case a https redirect is needed.
			string commandName = context.Request.Path;
			logger.Info("Processing request for " + commandName + ".");	

			/*if(
				context.Request.UserHostAddress.StartsWith("59") ||		// China
				context.Request.UserHostAddress.StartsWith("58") ||		// China
				context.Request.UserHostAddress.StartsWith("218") ||	// China
				context.Request.UserHostAddress.StartsWith("219") ||	// Bahrain
				context.Request.UserHostAddress.StartsWith("220") ||	// Hong Kong
				context.Request.UserHostAddress.StartsWith("221") ||	// China
				context.Request.UserHostAddress.StartsWith("222") ||	// China
				context.Request.UserHostAddress.StartsWith("82.201")	// Egypt
				
			 )
			{
				logger.Error("Throwing a 404 to Chinese IP address for request, " + commandName + ".");
				throw new HttpException(404, "Page not found.");
			}*/

			visitorLog.Info(DateTime.Now.ToString("dd MMM yy HH:mm:ss") + ", host IP = " + context.Request.UserHostAddress + ", request = " + context.Request.Path + ", ssl = " + context.Request.IsSecureConnection + ".");

			// Incoming request paths are in the format /<WebAppName>/<RelativeDirectory>/<PageName>.aspx.
			try
			{
				//commandName = Regex.Match(commandName, @"(\/.)*?\/(?<command>.*)\.\w*$").Result("${command}");

				if(m_virtualDir != null && m_virtualDir.Trim().Length > 0)
				{
					string regexCommandPattern = @"^\/" + m_virtualDir + @"\/(?<command>.*)\.\w*?$";
					commandName = Regex.Match(commandName, regexCommandPattern, RegexOptions.IgnoreCase).Result("${command}");
				}
				else
				{
					commandName = Regex.Match(commandName, @"^\/(?<command>.*)\.\w*?$").Result("${command}");
				}
			}
			catch
			{
				logger.Error("Error in ProcessRequest, could not extract the command name from the request.");
				throw new ApplicationException("Could not find a match for the page request, " + commandName + ".");
			}

			//logger.Debug("Extracted command name = " + commandName + ".");

			// Attempt to retrieve a matching command from the workflow map.
			PageCommand command = new PageCommand();

			// Attempt to find a perfect match, if not found attempt a regular expression match.
			if(m_reloadCommandPattern != null && Regex.Match(commandName, m_reloadCommandPattern, RegexOptions.IgnoreCase).Success)
			{
				logger.Debug("Match for reload command, command = " + commandName + ", pattern = " + m_reloadCommandPattern + ".");
				
				// Initialise the workflow map.
				Init(m_configFileName);

				// Change the comamnd name to "default".
				commandName = "default";
			}

			if(CommandMap.Contains(commandName))
			{
				logger.Debug("Equality match found for, " + commandName + ".");
				command = CreatePageCommand((XmlElement)CommandMap[commandName]);
			}
			else if(CommandMap.Contains("/" + commandName))
			{
				logger.Debug("Equality match found for, /" + commandName + ".");
				command = CreatePageCommand((XmlElement)CommandMap["/" + commandName]);
			}
			else
			{
				foreach(object commandPattern in CommandMap.Keys)
				{			
					if(Regex.Match(Regex.Escape(commandName), commandPattern.ToString(), RegexOptions.IgnoreCase).Success)
					{
						logger.Debug("Command Map match for request, " + commandName + ", and map item, " + commandPattern.ToString() + ".");
						command = CreatePageCommand((XmlElement)CommandMap[commandPattern]);
						break;
					}
				}
			}

			if(command.Controller == null) 
			{
				logger.Error("A page request was received for an unknown command, " + commandName + ".");
				throw new HttpException(404, "The requested page does not have a command mapping, " + commandName + ".");
			} 
			else 
			{
				//logger.Info("Servicing command " + commandName + ".");
				
				if(command.SSLRequired && !context.Request.IsSecureConnection)
				{
					// Redirect page to a https version.
					string sslUrl = "https://" + context.Request.ServerVariables["SERVER_NAME"] + originalRequest;
					logger.Debug("Redirecting due to forced SSL to: " + sslUrl);

					context.Response.Redirect(sslUrl, true);
				}

				// Process the page request.
				command.Controller.Go(context);
			}
		}
	
		public PageCommand CreatePageCommand(XmlElement commandNode)
		{
			PageCommand pageCommand = new PageCommand();
						
			ControllerProduct controller = ControllerFactory.CreateController(commandNode);
			pageCommand.Controller = controller;

			string sslRequired = null;
					
			try
			{
				sslRequired = commandNode.Attributes.GetNamedItem(SSL_REQUIRED_ATTRIBUTE).Value;
			}
			catch{}

			bool forceSSL = false;
			if(sslRequired != null && sslRequired.Length != 0)
			{
				if(Regex.Match(sslRequired, "true", RegexOptions.IgnoreCase).Success)
				{
					forceSSL = true;
				}

				pageCommand.SSLRequired = forceSSL;
			}

			return pageCommand;
		}
		
		/// <summary>
		/// Reloads the xml configuration data.  Can be done on-the-fly.  Any
		/// requests being serviced are allowed to complete with the old data.
		/// This method will be called relatively infrequently so using the XML
		/// DOM parser is acceptable.
		/// </summary>
		/// <param name="configFilePath">The physical path to the MaverickLite 
		/// configuration file.</param>
		private static void Init(string configFilePath)
		{
			XmlDocument configDocument = new XmlDocument();
			configDocument.Load(configFilePath);

			logger.Info("Maverick configuration file loaded from " + configFilePath + ".");

			Init(configDocument);
		}

		private static void Init(XmlDocument configDocument)
		{
			try
			{
				//logger.Debug("Starting Dispatcher initialisation.");

				// Dictionary to load commands into.
				IDictionary initCommands = new Hashtable(new CaseInsensitiveHashCodeProvider(), new CaseInsensitiveComparer());

				// Load commands; can be many elements.
				foreach(XmlElement commandNode in configDocument.SelectNodes(COMMAND_NODES_PATH)) 
				{
					string commandName = commandNode.Attributes.GetNamedItem(COMMAND_NAME_ATTRIBUTE).Value;
			
					//logger.Debug("Processing command string " + commandName);

					// Split each command on the "|" character to allow different names for the same page.
					string[] multipleCommands = Regex.Split(commandName, @"\|");

					foreach(string singleCommand in multipleCommands)
					{
						//logger.Info("Creating command: " + singleCommand);

						//logger.Debug(commandNode.OuterXml);
						initCommands.Add(singleCommand, commandNode);
					}
				}

				// Place the loaded commands into the workflow map.
				CommandMap = initCommands;
				XmlConfigDoc = configDocument;	

				// Initialise the reload command pateern.
				if(m_reloadCommand != null)
				{
					m_reloadCommandPattern = "^" + m_reloadCommand + "$";
				}

				logger.Debug("Dispatcher initialisation completed.");
			}
			catch(Exception excp)
			{
				logger.Error("An exception occurred initialising the Dispatcher (Dispatcher.Init). " + excp.Message);
				throw excp;
			}
		}

		#region Unit tests.

		#if UNITTEST

		#region Support classes for unit testing.

		
		/// <summary>
		/// This is a mock class to allow isolated testing of the Dispatcher
		/// class without having to worry about MavMavAppState.
		/// </summary>
		class MavMavAppState
		{
			private const string MAVERICKLITE_TEST_CONFIG_PATH = "mavericklite.unittest.xml";
			
			public static log4net.ILog logger = null;
			public static log4net.ILog VisitorLog = null;

			public static string MaverickConfigFile;
			public static string PhysicalApplicationPath;
			public static readonly string VirtualDir;

			static MavMavAppState()
			{
				// Configure a console logger to send log messages to the console.
				log4net.Appender.ConsoleAppender appender = new log4net.Appender.ConsoleAppender();

				log4net.Layout.ILayout fallbackLayout = new log4net.Layout.PatternLayout("Log Msg: %m%n");
				appender.Layout = fallbackLayout;
			
				log4net.Config.BasicConfigurator.Configure(appender);

				logger = log4net.LogManager.GetLogger(string.Empty);
				VisitorLog = log4net.LogManager.GetLogger(string.Empty);

				// Initialise the required application state variables.
				PhysicalApplicationPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase + @"\";
				MaverickConfigFile = PhysicalApplicationPath + MAVERICKLITE_TEST_CONFIG_PATH;	
			}
		}
		

		/// <summary>
		/// This is a mock class to allow isolated testing of the Dispatcher
		/// class without having to worry about Controllers. The Go method in this class updates
		/// a static field with the name of the last run command, this allows unit tests to check that
		/// the correct command was chosen.
		/// </summary>
		public class ControllerProduct
		{
			public static string LastCommandName;	// Holds the name of the last command run.
			
			public string CommandName;				// Holds the name of the command for each controller instance.
			
			public ControllerProduct(XmlElement commandNode)
			{
				CommandName = commandNode.Attributes.GetNamedItem(COMMAND_NAME_ATTRIBUTE).Value;
			}

			public void Go(HttpContext context)
			{
				LastCommandName = CommandName;
			}
		}

		/// <summary>
		/// This is a mock class to allow isolated testing of the Dispatcher
		/// class without having to worry about ControllerFactory.
		/// </summary>
		class ControllerFactory
		{
			public static ControllerProduct CreateController(XmlElement commandNode)
			{
				ControllerProduct mockController = new ControllerProduct(commandNode);

				return mockController;
			}
		}

		/// <summary>
		/// Mock class to allow customised HttpContexts to be created and passed to the Dispatcher.
		/// TODO: Work out how to catch the Response stream.
		/// </summary>
		class MockWebRequest : HttpWorkerRequest
		{
			public string UriPath;
			
			public MockWebRequest() : base()
			{
				//Response = new HttpResponse(Console.Out);
				//Response.ContentType = "test";
			}

			public override void EndOfRequest()
			{}

			public override void FlushResponse(bool finalResponse)
			{}

			public override string GetHttpVerbName()
			{
				return "GET";
			}

			public override string GetHttpVersion()
			{
				return "1.1";
			}

			public override string GetLocalAddress()
			{
				return "127.0.0.1";
			}

			public override int GetLocalPort()
			{
				return 80;
			}

			public override string GetQueryString()
			{
				return "x";
			}

			public override string GetRawUrl()
			{
				return "y";
			}

			public override string GetRemoteAddress()
			{
				return "127.0.0.1";
			}

			public override int GetRemotePort()
			{
				return 0;
			}

			public override string GetUriPath()
			{
				return UriPath;
			}

			public override void SendResponseFromFile(IntPtr filePtr, long offset, long length)
			{}

			public override void SendResponseFromFile(string filePath, long offset, long length)
			{}

			public override void SendKnownResponseHeader(int index, string headerValue)
			{}

			public override void SendResponseFromMemory(byte[] response, int length)
			{}

			public override void SendResponseFromMemory(IntPtr response, int length)
			{}

			public override void SendStatus(int statusCode, string statusDescription)
			{}

			public override void SendUnknownResponseHeader(string name, string headerValue)
			{}
		}

		
		#endregion

		[TestFixture]
		public class DispatcherUnitTest
		{		
			private string m_physicalApplicationPath;
			private string m_maverickConfigFile;

			private XmlDocument testMavLiteConfigDom = new XmlDocument();
			
			/// <summary>
			/// Initialises settings required for the Dispatcher unit tests.
			/// </summary>
			[SetUp]
			public void UnitTestInit()
			{
				m_physicalApplicationPath = MavMavAppState.PhysicalApplicationPath;
				m_maverickConfigFile = MavMavAppState.MaverickConfigFile;
				
				Console.WriteLine("Unit test MaverickLite configuration file path = " + m_maverickConfigFile + ".");

				// Create a test config file.
				string testConfigContents = 
					@"<mavericklite>
						<commands>
							<command name='command1'>
								<controller/>
							</command>
							<command name='command2'>
								<controller/>
							</command>
							<command name='command.{0,1}'>
								<controller/>
							</command>		
							<command name='(?&lt;!test)\/.*'>
								<controller/>
							</command>
							<command name='dir1/dir2/call' />
							<command name='dir1/dir2/calledit' />		
						</commands>
					  </mavericklite>";

				testMavLiteConfigDom.LoadXml(testConfigContents);
				
				XmlTextWriter testConfigWriter = new XmlTextWriter(m_maverickConfigFile, System.Text.Encoding.ASCII);
				testConfigWriter.Formatting = Formatting.Indented;
				testConfigWriter.Indentation = 4;

				testMavLiteConfigDom.WriteTo(testConfigWriter);
				testConfigWriter.Close();
			}

			/*
			/// <summary>
			/// Bit tricky to test static constructors using inner classes.
			/// </summary>
			[Test]
			[ExpectedException(typeof(System.TypeInitializationException))]
			public void DispatcherConstructorTest()
			{
				MavMavAppState.MaverickConfigFile = null;	
			}*/
				
			/// <summary>
			/// Tests that the MaverickLite configuration file is parsed correctly.
			/// </summary>
			[Test]
			public void InitTest()
			{
				Console.WriteLine("=> Test " + System.Reflection.MethodBase.GetCurrentMethod().Name);
				
				//MavMavAppState.MaverickConfigFile = m_maverickConfigFile;

				Init(m_maverickConfigFile);
		
				// Check that commands were correctly loaded.
				Assert.IsTrue(CommandMap.Count == 6, "Commands were not correctly loaded.");
			}

			[Test]
			[ExpectedException(typeof(System.ArgumentException))]
			public void NoConfigFileInit()
			{
				Console.WriteLine("=> Test " + System.Reflection.MethodBase.GetCurrentMethod().Name);
				
				string fileName = null;
				
				Init(fileName);
			}

			/// <summary>
			/// Tests that a simple valid command is acted on without any exceptions being thrown.
			/// </summary>
			[Test]
			public void SimplePageRequestTest()
			{
				Console.WriteLine("=> Test " + System.Reflection.MethodBase.GetCurrentMethod().Name);
				
				MockWebRequest mockRequest = new MockWebRequest();
				mockRequest.UriPath = "/test/command1.aspx";

				HttpContext mockContext = new HttpContext(mockRequest);

				Dispatcher testDispatcher = new Dispatcher();
				testDispatcher.ProcessRequest(mockContext);	
			}


			/// <summary>
			/// Tests that the correct command is chosen for a specific request when a perfect match exists.
			/// </summary>
			[Test]
			public void PerfectMatchPageRequestTest()
			{
				Console.WriteLine("=> Test " + System.Reflection.MethodBase.GetCurrentMethod().Name);
				
				MockWebRequest mockRequest = new MockWebRequest();
				mockRequest.UriPath = "/command1.aspx";

				HttpContext mockContext = new HttpContext(mockRequest);

				Dispatcher testDispatcher = new Dispatcher();
				testDispatcher.ProcessRequest(mockContext);
				
				string commandName = ControllerProduct.LastCommandName;

				Assert.AreEqual(commandName, "command1", "Incorrect command chosen, " + commandName + ", should have been command1.");
			}

			/// <summary>
			/// Tests that the correct command is chosen for a request decided by a regular expression.
			/// </summary>
			[Test]
			public void SpecificRegexPageRequestTest()
			{			
				Console.WriteLine("=> Test " + System.Reflection.MethodBase.GetCurrentMethod().Name);
				
				MockWebRequest mockRequest = new MockWebRequest();
				mockRequest.UriPath = "/test/commandX.aspx";

				HttpContext mockContext = new HttpContext(mockRequest);

				Dispatcher testDispatcher = new Dispatcher();
				testDispatcher.ProcessRequest(mockContext);
				
				string commandName = ControllerProduct.LastCommandName;

				Assert.AreEqual(commandName, "command.{0,1}", "Incorrect command chosen, " + commandName + ", should have been command.{0,1}");
			}

			/// <summary>
			/// Tests that the correct command is chosen when one command is similar to another.
			/// </summary>
			[Test]
			public void SimilarCommandsTest()
			{			
				Console.WriteLine("=> Test " + System.Reflection.MethodBase.GetCurrentMethod().Name);
				
				MockWebRequest mockRequest = new MockWebRequest();
				mockRequest.UriPath = "/dir1/dir2/call.aspx";

				HttpContext mockContext = new HttpContext(mockRequest);

				Dispatcher testDispatcher = new Dispatcher();
				testDispatcher.ProcessRequest(mockContext);
				
				string commandName = ControllerProduct.LastCommandName;

				Assert.AreEqual(commandName, "dir1/dir2/call", "Incorrect command chosen, " + commandName + ", should have been dir1/dir2/call");

				mockRequest.UriPath = "/dir1/dir2/calledit.aspx";
				mockContext = new HttpContext(mockRequest);

				testDispatcher.ProcessRequest(mockContext);
				
				commandName = ControllerProduct.LastCommandName;

				Assert.AreEqual(commandName, "dir1/dir2/calledit", "Incorrect command chosen, " + commandName + ", should have been dir1/dir2/calledit");
			}


			/// <summary>
			/// Tests that the correct command is chosen for a request when only a general 
			/// regular expression exists.
			/// </summary>
			[Test]
			public void GenericRegexPageRequestTest()
			{
				Console.WriteLine("=> Test " + System.Reflection.MethodBase.GetCurrentMethod().Name);
				
				MockWebRequest mockRequest = new MockWebRequest();
				mockRequest.UriPath = "/testx/abcd.aspx";

				HttpContext mockContext = new HttpContext(mockRequest);

				Dispatcher testDispatcher = new Dispatcher();
				testDispatcher.ProcessRequest(mockContext);
				
				string commandName = ControllerProduct.LastCommandName;

				Assert.AreEqual(commandName, @"(?<!test)\/.*", "Incorrect command chosen, " + commandName + @", should have been (?<!test)\/.*");
			}

			
			/// <summary>
			/// Tests that the no command is chosen for a request when a command mapping does not exist.
			/// </summary>
			[Test]
			[ExpectedException(typeof(System.Web.HttpException))]
			public void NoCommandPageRequestTest()
			{
				Console.WriteLine("=> Test " + System.Reflection.MethodBase.GetCurrentMethod().Name);
				
				MockWebRequest mockRequest = new MockWebRequest();
				mockRequest.UriPath = "/test/1cmd.aspx";

				HttpContext mockContext = new HttpContext(mockRequest);

				Dispatcher testDispatcher = new Dispatcher();
				testDispatcher.ProcessRequest(mockContext);
			}	
		}

		#endif

		#endregion
	}
}
